package com.atguigu.gmall.common.config;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.entity.GmallCorrelationData;
import com.atguigu.gmall.common.service.RabbitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author wk
 * @create 2021-09-12 18:27
 * <p>
 * 消息应答配置类
 * 通过实现 确认回调接口 确保交换机回调应答
 * 通过实现 返回回调接口 确保队列回调应答
 */
@Slf4j
@Component
public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * ConfirmCallback ，ReturnCallback 这两个接口属于RabbitTemplate的内部接口
     * 想要调用 内部接口的方法 需要将实现类的对象设置到接口中
     * <p>
     * 此时不能使用构造器 因为构造器的初始化时间早于  @Autowired RabbitTemplate rabbitTemplate 的注入
     * 会报空指针异常
     * <p>
     * 可以通过方法设置 通过在方法添加@PostConstruct 可以在当前类实例化之后去执行某些操作
     */
    @PostConstruct  // 作用的是在Bean初始化方法后执行
    public void MQProducerAckConfig() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    // 交换机回调应答

    /**
     * ack :  true:交换机成功应答
     * false:交换机失败应答
     * cause： 成功了：null
     * 失败了：失败的原因
     * CorrelationData：附加对象
     * 里面：交换机名称 、RoutingKey、Message、是否是延迟消息 延迟时间 ..
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("交换机成功应答");
        } else {
            log.error("交换机应答失败" + "原因是" + cause);
            // 失败了重新发送
            retrySendMessage(correlationData);
        }
    }

    private void retrySendMessage(CorrelationData correlationData) {
        // 将correlationData强转为在发送消息时设置GmallCorrelationData对象
        GmallCorrelationData gmallCorrelationData = (GmallCorrelationData) correlationData;
        int retryCount = gmallCorrelationData.getRetryCount();
        // 可以通过判断重试的次数 决定是否要继续发送还是打印日志 人工介入
        if (retryCount < 2) {
            retryCount++;
            gmallCorrelationData.setRetryCount(retryCount);
            // 更新缓存
            String lId = ((GmallCorrelationData) correlationData).getExchange() + gmallCorrelationData.getRoutingKey();
            redisTemplate.opsForHash().put(lId, gmallCorrelationData.getId(), JSONObject.toJSON(gmallCorrelationData));
            // 重试
            rabbitTemplate.convertAndSend(
                    gmallCorrelationData.getExchange(),
                    gmallCorrelationData.getRoutingKey(),
                    gmallCorrelationData.getMessage(),
                    gmallCorrelationData);
        } else {
            log.error("重试次数已达到上限 无法继续重试");
        }

    }

    /**
     * 队列应答的流程
     * 生产者在发送消息之前 将gmallCorrelationData对象存到缓存中 待队列接收消息失败时
     * 回调可以从缓存中获取相关交换机 路由键等 然后生产者再次发送消息
     */
    // 队列回调应答  只有失败才会回调
    @Override
    public void returnedMessage(Message message, int replyCode,
                                String replyText, String exchange, String routingKey) {
        log.error("队列失败应答：消息：" + new String(message.getBody()));
        log.error("队列失败应答：失败码：" + replyCode);
        log.error("队列失败应答：失败消息：" + replyText);
        log.error("队列失败应答：交换机：" + exchange);
        log.error("队列失败应答：routingKey：" + routingKey);

        // 队列确认应答
        String lId = exchange + routingKey;
        Object kId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        // 从缓存中获取 可以通过debug 查看 uuid
        Object o = redisTemplate.opsForHash().get(lId, kId);
        String s = JSONObject.toJSONString(o);
        // 对象转换
        GmallCorrelationData gmallCorrelationData = JSONObject.parseObject(s, GmallCorrelationData.class);
        if (gmallCorrelationData.isDelayedDelay()) {
            // 如果是基于插件的延迟消息
            System.out.println("本次是基于插件的延迟消息");
            return;
        }
        // 重新发送
        retrySendMessage(gmallCorrelationData);
    }
}
